package cn.lastjune.dao.impl;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.transform.ResultTransformer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import cn.lastjune.util.Page;
import cn.lastjune.util.PropertyFilter;
import cn.lastjune.util.PropertyFilter.MatchType;

/**
 * 
 * @author Timiz
 * 
 * @param <T>
 * @param <ID>
 */
@SuppressWarnings("unchecked")
public class HibernateDao<T, ID extends Serializable> {

	protected final Log logger = LogFactory.getLog(getClass());

	protected SessionFactory sessionFactory;

	protected Class<T> entityClass;

	/**
	 * HibernateDao Constructor, set entityClass
	 */
	public HibernateDao() {
		this.entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
	}

	/**
	 * HibernateDao Constructor
	 * 
	 * @param sessionFactory Hibernate sessionFactory
	 * @param entityClass clazz for <T>
	 */
	public HibernateDao(final SessionFactory sessionFactory, final Class<T> entityClass) {
		this.sessionFactory = sessionFactory;
		this.entityClass = entityClass;
	}

	/**
	 * Get sessionFactory.
	 * 
	 * @return Hibernate sessionFactory
	 */
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * Auto inject sessionFactory by "@Autowired"
	 * 
	 * @param sessionFactory Hibernate sessionFactory
	 */
	@Autowired
	public void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * Get current session.
	 * 
	 * @return current session
	 */
	public Session getSession() {
		return sessionFactory.getCurrentSession();
	}

	// -- Basic CRUD Function --//

	/**
	 * Save new entity
	 * 
	 * @param entity entity object
	 * @return saved entity's id
	 */
	public ID add(final T entity) {
		Assert.notNull(entity, "entity must not be null");
		final ID id = (ID) getSession().save(entity);
		getSession().flush();
		return id;
	}

	/**
	 * Update entity
	 * 
	 * @param entity updated entity object
	 */
	public void update(final T entity) {
		Assert.notNull(entity, "entity must not be null");
		getSession().update(entity);
		getSession().flush();
	}

	/**
	 * Save or update object
	 * 
	 * @param entity entity object
	 */
	public void saveOrUpdate(final T entity) {
		Assert.notNull(entity, "entity must not be null");
		getSession().saveOrUpdate(entity);
		getSession().flush();
	}

	/**
	 * Delete entity
	 * 
	 * @param entity entity object
	 */
	public void delete(final T entity) {
		Assert.notNull(entity, "entity must not be null");
		if (null != entity) {
			getSession().delete(entity);
			getSession().flush();
		}
	}

	/**
	 * Delete entity by id
	 * 
	 * @param id entity id
	 */
	public void delete(final ID id) {
		Assert.notNull(id, "id must not be null");
		final T obj = get(id);
		if (null != obj) {
			delete(obj);
		}
	}

	/**
	 * Get object by id
	 * 
	 * @param id entity id
	 * @return entity
	 */
	public T get(final ID id) {
		Assert.notNull(id, "id must not be null");
		return (T) getSession().get(entityClass, id);
	}

	/**
	 * Load object by id
	 * 
	 * @param id entity id
	 * @return entity
	 */
	public T load(final ID id) {
		Assert.notNull(id, "id must not be null");
		return (T) getSession().load(entityClass, id);
	}

	/**
	 * Get all entities
	 * 
	 * @return entity list
	 */
	public List<T> getAll() {
		return get();
	}

	/**
	 * Get all objects by order
	 * 
	 * @param orderBy order by column
	 * @param isAsc order is asc or not
	 * @return entity list
	 */
	public List<T> getAll(final String orderBy, final boolean isAsc) {
		final Criteria c = createCriteria();
		if (isAsc) {
			c.addOrder(Order.asc(orderBy));
		} else {
			c.addOrder(Order.desc(orderBy));
		}
		return c.list();
	}

	/**
	 * Get objects by property
	 * 
	 * @param propertyName query property
	 * @param value expected property value
	 * @return entity list
	 */
	public List<T> getByProperty(final String propertyName, final Object value) {
		return getByProperty(propertyName, value, MatchType.EQ);
	}

	/**
	 * Get objects by property, support multi compare conditions: public enum MatchType { EQ, LIKE, LT, GT, LE, GE; }
	 * 
	 * @param propertyName query property
	 * @param value expected property value
	 * @param matchType compare conditions
	 * @return entity list
	 */
	public List<T> getByProperty(final String propertyName, final Object value, final MatchType matchType) {
		final Criterion criterion = buildPropertyFilterCriterion(propertyName, value, matchType);
		return get(criterion);
	}

	/**
	 * Get unique object by property
	 * 
	 * @param propertyName query property
	 * @param value expected property value
	 * @return unique entity
	 */
	public T getUniqueByProperty(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName must not be null");
		final Criterion criterion = Restrictions.eq(propertyName, value);
		return (T) createCriteria(criterion).uniqueResult();
	}

	/**
	 * Judge whether unique value of certain property already exist in table
	 * 
	 * @param propertyName property name
	 * @param value unique value
	 * @return true or false
	 */
	public boolean isPropertyExist(final String propertyName, final Object value) {
		return getUniqueByProperty(propertyName, value) == null;
	}

	/**
	 * Get objects by ids
	 * 
	 * @param ids entities' id list
	 * @return entity list
	 */
	public List<T> getByIds(final List<ID> ids) {
		return get(Restrictions.in(getIdName(), ids));
	}

	/**
	 * Query by HQL
	 * 
	 * @param hql HQL query string
	 * @param <X> any conent in list
	 * @param values HQL parameters
	 * @return list which meets HQL query
	 */
	public <X> List<X> get(final String hql, final Object... values) {
		return createQuery(hql, values).list();
	}

	/**
	 * Query by HQL
	 * 
	 * @param hql HQL query string
	 * @param values named HQL parameters
	 * @param <X> any object in list
	 * @return list which meets HQL query
	 */
	public <X> List<X> get(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).list();
	}

	/**
	 * get entities by filers
	 * 
	 * @param filters compared filter
	 * @return entity list
	 */
	public List<T> get(final List<PropertyFilter> filters) {
		final Criterion[] criterions = buildPropertyFilterCriterions(filters);
		return get(criterions);
	}

	/**
	 * Get objects by criteria
	 * 
	 * @param criterions Criterions
	 * @return entity objects
	 */
	public List<T> get(final Criterion... criterions) {
		return createCriteria(criterions).list();
	}

	/**
	 * Query unique object by HQL
	 * 
	 * @param hql HQL query string
	 * @param <X> any object
	 * @param values HQL parameters
	 * @return unique object which meets HQL query
	 */
	public <X> X getUnique(final String hql, final Object... values) {
		return (X) createQuery(hql, values).uniqueResult();
	}

	/**
	 * Query unique object by HQL
	 * 
	 * @param hql HQL query string
	 * @param <X> any object
	 * @param values HQL named parameters
	 * @return unique object which meets HQL query
	 */
	public <X> X getUnique(final String hql, final Map<String, ?> values) {
		return (X) createQuery(hql, values).uniqueResult();
	}

	/**
	 * Execute batch HQL update/delete operation
	 * 
	 * @param hql HQL query string
	 * @param values HQL parameters
	 * @return effected records number
	 */
	public int batchExecute(final String hql, final Object... values) {
		return createQuery(hql, values).executeUpdate();
	}

	/**
	 * Execute batch HQL update/delete operation
	 * 
	 * @param hql HQL query string
	 * @param values HQL named parameters
	 * @return effected records number
	 */
	public int batchExecute(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).executeUpdate();
	}

	/**
	 * Create query object
	 * 
	 * @param queryString HQL query string
	 * @param values HQL parameters
	 * @return Query object
	 */
	public Query createQuery(final String queryString, final Object... values) {
		Assert.hasText(queryString, "queryString must not be null");
		final Query query = getSession().createQuery(queryString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				query.setParameter(i, values[i]);
			}
		}
		return query;
	}

	/**
	 * Create query object
	 * 
	 * @param queryString HQL query string
	 * @param values HQL named parameters
	 * @return Query object
	 */
	public Query createQuery(final String queryString, final Map<String, ?> values) {
		Assert.hasText(queryString, "queryString must not be null");
		final Query query = getSession().createQuery(queryString);
		if (values != null) {
			query.setProperties(values);
		}
		return query;
	}

	/**
	 * Get unique object by criteria
	 * 
	 * @param criterions Criterions
	 * @return unique entity
	 */
	public T getUnique(final Criterion... criterions) {
		return (T) createCriteria(criterions).uniqueResult();
	}

	/**
	 * Construct Criteria
	 * 
	 * @param criterions Criterions
	 * @return Criteria object
	 */
	public Criteria createCriteria(final Criterion... criterions) {
		final Criteria criteria = getSession().createCriteria(entityClass);
		for (final Criterion c : criterions) {
			criteria.add(c);
		}
		return criteria;
	}

	/**
	 * Get primary key's name
	 * 
	 * @return primary key's name
	 */
	public String getIdName() {
		final ClassMetadata meta = getSessionFactory().getClassMetadata(entityClass);
		return meta.getIdentifierPropertyName();
	}

	/**
	 * @param page page pagination helper object
	 * @param sql SQL query string
	 * @param values SQL parameters
	 * @return pagination object which includes entity list
	 */
	// Pagination Functions

	public Page<T> getPageBySQL(final Page<T> page, final String sql, final Object... values) {
		Assert.notNull(page, "page must not be null");

		final Query q = getSession().createSQLQuery(sql).addEntity(entityClass);

		for (int i = 0; i < values.length; i++) {
			q.setParameter(i, values[i]);
		}
		if (page.isAutoCount()) {
			final long totalCount = countSqlResult(sql, values);
			page.setTotalCount(totalCount);
		}

		setPageParameter(q, page);
		final List result = q.list();
		page.setResult(result);
		return page;
	}

	/**
	 * get all pagination entities
	 * 
	 * @param page pagination helper object
	 * @return pagination object which includes entity list
	 */
	public Page<T> getPageAll(final Page<T> page) {
		return getPage(page);
	}

	/**
	 * get pagination entities by HQL
	 * 
	 * @param page pagination helper object
	 * @param hql HQL query string
	 * @param values HQL parameters
	 * @return pagination object which includes entity list
	 */
	public Page<T> getPage(final Page<T> page, final String hql, final Object... values) {
		Assert.notNull(page, "page must not be null");

		final Query q = createQuery(hql, values);

		if (page.isAutoCount()) {
			final long totalCount = countHqlResult(hql, values);
			page.setTotalCount(totalCount);
		}

		setPageParameter(q, page);
		final List result = q.list();
		page.setResult(result);
		return page;
	}

	/**
	 * get pagination entities by HQL
	 * 
	 * @param page pagination helper object
	 * @param hql HQL query string
	 * @param values HQL named parameters
	 * @return pagination object which includes entity list
	 */
	public Page<T> getPage(final Page<T> page, final String hql, final Map<String, Object> values) {
		Assert.notNull(page, "page must not be null");

		final Query q = createQuery(hql, values);

		if (page.isAutoCount()) {
			final long totalCount = countHqlResult(hql, values);
			page.setTotalCount(totalCount);
		}

		setPageParameter(q, page);

		final List result = q.list();
		page.setResult(result);
		return page;
	}

	/**
	 * get pagination entities by filers
	 * 
	 * @param page pagination helper object
	 * @param filters compared filter
	 * @return pagination object which includes entity list
	 */
	public Page<T> getPage(final Page<T> page, final List<PropertyFilter> filters) {
		final Criterion[] criterions = buildPropertyFilterCriterions(filters);
		return getPage(page, criterions);
	}

	/**
	 * get pagination entities by criteria
	 * 
	 * @param page pagination helper object
	 * @param criterions Criterions
	 * @return pagination object which includes entity list
	 */
	public Page<T> getPage(final Page<T> page, final Criterion... criterions) {
		Assert.notNull(page, "page must not be null");

		final Criteria c = createCriteria(criterions);

		if (page.isAutoCount()) {
			final int totalCount = countCriteriaResult(c);
			page.setTotalCount(totalCount);
		}
		setPageParameter(c, page);
		final List result = c.list();
		page.setResult(result);
		return page;
	}

	protected Query setPageParameter(final Query q, final Page<T> page) {
		// hibernate's firstResult start with 0
		q.setFirstResult(page.getFirst() - 1);
		q.setMaxResults(page.getPageSize());
		return q;
	}

	protected Criteria setPageParameter(final Criteria c, final Page<T> page) {
		// hibernate's firstResult start with 0
		c.setFirstResult(page.getFirst() - 1);
		c.setMaxResults(page.getPageSize());

		if (page.isOrderBySetted()) {
			final String[] orderByArray = StringUtils.split(page.getOrderBy(), ',');
			final String[] orderArray = StringUtils.split(page.getOrder(), ',');

			Assert.isTrue(orderByArray.length == orderArray.length, "the number of orderBy and order are not equal");

			for (int i = 0; i < orderByArray.length; i++) {
				if (Page.ASC.equals(orderArray[i])) {
					c.addOrder(Order.asc(orderByArray[i]).ignoreCase());
				} else {
					c.addOrder(Order.desc(orderByArray[i]).ignoreCase());
				}
			}
		}
		return c;
	}

	protected long countHqlResult(final String hql, final Object... values) {
		String fromHql = hql;
		final int indexOfDistinct = fromHql.indexOf("distinct");
		if (indexOfDistinct != -1) {
			final int indexOfFrom = fromHql.indexOf("from");
			final String distinctStr = fromHql.substring(indexOfDistinct, indexOfFrom);
			fromHql = "select count(" + distinctStr + ")from " + StringUtils.substringAfter(fromHql, "from");
		} else {
			fromHql = "select count(*) from " + StringUtils.substringAfter(fromHql, "from");
		}
		fromHql = StringUtils.substringBefore(fromHql, "order by");

		try {
			final Long count = getUnique(fromHql, values);
			return count;
		} catch (final Exception e) {
			throw new RuntimeException("hql can't be auto count, hql is:" + fromHql, e);
		}
	}

	protected long countSqlResult(final String sql, final Object... values) {

		final String countSql = "select count(*) counter from (" + sql + ") tempTable";
		final Query query = getSession().createSQLQuery(countSql).addScalar("counter", Hibernate.LONG);
		try {
			for (int i = 0; i < values.length; i++) {
				query.setParameter(i, values[i]);
			}
			final Long count = (Long) query.uniqueResult();
			return count;
		} catch (final Exception e) {
			throw new RuntimeException("hql can't be auto count, sql is:" + countSql, e);
		}
	}

	protected long countHqlResult(final String hql, final Map<String, Object> values) {
		String fromHql = hql;
		final int indexOfDistinct = fromHql.indexOf("distinct");
		if (indexOfDistinct != -1) {
			final int indexOfFrom = fromHql.indexOf("from");
			final String distinctStr = fromHql.substring(indexOfDistinct, indexOfFrom);
			fromHql = "select count(" + distinctStr + ")from " + StringUtils.substringAfter(fromHql, "from");
		} else {
			fromHql = "select count(*) from " + StringUtils.substringAfter(fromHql, "from");
		}
		fromHql = StringUtils.substringBefore(fromHql, "order by");

		try {
			final Long count = getUnique(fromHql, values);
			return count;
		} catch (final Exception e) {
			throw new RuntimeException("hql can't be auto count, hql is:" + fromHql, e);
		}
	}

	protected int countCriteriaResult(final Criteria c) {
		final CriteriaImpl impl = (CriteriaImpl) c;

		final Projection projection = impl.getProjection();
		final ResultTransformer transformer = impl.getResultTransformer();

		List<CriteriaImpl.OrderEntry> orderEntries = null;

		final Field field = ReflectionUtils.findField(impl.getClass(), "orderEntries");
		field.setAccessible(true);
		try {
			orderEntries = (List) ReflectionUtils.getField(field, impl);
			ReflectionUtils.setField(field, impl, new ArrayList());
		} catch (final Exception e) {
			e.printStackTrace();
			logger.error("reflectionUtils error");
		}

		final int totalCount = (Integer) c.setProjection(Projections.rowCount()).uniqueResult();

		c.setProjection(projection);

		if (projection == null) {
			c.setResultTransformer(CriteriaSpecification.ROOT_ENTITY);
		}
		if (transformer != null) {
			c.setResultTransformer(transformer);
		}
		try {
			ReflectionUtils.setField(field, impl, orderEntries);
		} catch (final Exception e) {
			logger.error("reflectionUtils error");
		}

		return totalCount;
	}

	/**
	 * Build criterions array according to compare conditions
	 */
	protected Criterion[] buildPropertyFilterCriterions(final List<PropertyFilter> filters) {
		final List<Criterion> criterionList = new ArrayList<Criterion>();
		for (final PropertyFilter filter : filters) {
			if (!filter.isMultiProperty()) { // only one cmopared property
				final Criterion criterion = buildPropertyFilterCriterion(filter.getPropertyName(),
						filter.getPropertyValue(), filter.getMatchType());
				criterionList.add(criterion);
			} else {// multi compared property, e.g A or B or C = "name"
				final Disjunction disjunction = Restrictions.disjunction();
				for (final String param : filter.getPropertyNames()) {
					final Criterion criterion = buildPropertyFilterCriterion(param, filter.getPropertyValue(),
							filter.getMatchType());
					disjunction.add(criterion);
				}
				criterionList.add(disjunction);
			}
		}
		return criterionList.toArray(new Criterion[criterionList.size()]);
	}

	/**
	 * Build criterion according to compare conditions
	 */
	protected Criterion buildPropertyFilterCriterion(final String propertyName, final Object propertyValue,
			final MatchType matchType) {
		Assert.hasText(propertyName, "propertyName must not be null");
		Criterion criterion = null;

		if (MatchType.EQ.equals(matchType)) {
			criterion = Restrictions.eq(propertyName, propertyValue);
		} else if (MatchType.LIKE.equals(matchType)) {
			criterion = Restrictions.like(propertyName, (String) propertyValue, MatchMode.ANYWHERE);
		} else if (MatchType.ILIKE.equals(matchType)) {
			criterion = Restrictions.ilike(propertyName, (String) propertyValue, MatchMode.ANYWHERE);
		} else if (MatchType.LE.equals(matchType)) {
			criterion = Restrictions.le(propertyName, propertyValue);
		} else if (MatchType.LT.equals(matchType)) {
			criterion = Restrictions.lt(propertyName, propertyValue);
		} else if (MatchType.GE.equals(matchType)) {
			criterion = Restrictions.ge(propertyName, propertyValue);
		} else if (MatchType.GT.equals(matchType)) {
			criterion = Restrictions.gt(propertyName, propertyValue);
		}

		return criterion;
	}
}
